React Suspense๋ฅผ ํ์ฉํ ๋ณ๋ ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ๊ณ ๊ธ ๊ธฐ๋ฒ์ ํ๊ตฌํ์ฌ ์ฑ ์ฑ๋ฅ๊ณผ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค์ธ์. ์ฌ๋ฌ ๋น๋๊ธฐ ์์ ์ ์กฐ์จํ๊ณ ๋ก๋ฉ ์ํ๋ฅผ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ๋ ์ ๋ต์ ๋ฐฐ์๋๋ค.
React Suspense ์กฐ์จ: ๋ณ๋ ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ๋ง์คํฐํ๊ธฐ
React Suspense๋ ๋น๋๊ธฐ ์์ , ํนํ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ์ฒ๋ฆฌํ๋ ๋ฐฉ์์ ํ๋ช ์ ์ผ์ผ์ผฐ์ต๋๋ค. ์ด๋ฅผ ํตํด ๊ตฌ์ฑ ์์๋ ๋ฐ์ดํฐ ๋ก๋๋ฅผ ๊ธฐ๋ค๋ฆฌ๋ ๋์ ๋ ๋๋ง์ "์ผ์ ์ค์ง"ํ ์ ์์ผ๋ฉฐ, ๋ก๋ฉ ์ํ๋ฅผ ๊ด๋ฆฌํ๋ ์ ์ธ์ ์ธ ๋ฐฉ๋ฒ์ ์ ๊ณตํฉ๋๋ค. ๊ทธ๋ฌ๋ ๊ฐ๋ณ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ๋จ์ํ Suspense๋ก ๊ฐ์ธ๋ ๊ฒ์ ํ ๋ฒ์ ๊ฐ์ ธ์ค๊ธฐ๊ฐ ์๋ฃ๋ ํ์ ๋ค์ ๊ฐ์ ธ์ค๊ธฐ๊ฐ ์์๋๋ ํญํฌ์ ํจ๊ณผ๋ฅผ ์ด๋ํ์ฌ ์ฑ๋ฅ์ ๋ถ์ ์ ์ธ ์ํฅ์ ๋ฏธ์น ์ ์์ต๋๋ค. ์ด ๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ์์๋ Suspense๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ๋ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ๋ณ๋ ฌ๋ก ์กฐ์จํ๊ธฐ ์ํ ๊ณ ๊ธ ์ ๋ต์ ๊น์ด ํ๊ณ ๋ค์ด, ์ ํ๋ฆฌ์ผ์ด์ ์ ์๋ต์ฑ์ ์ต์ ํํ๊ณ ์ ์ธ๊ณ ์ฌ์ฉ์๋ฅผ ์ํ ์ฌ์ฉ์ ๊ฒฝํ์ ํฅ์์ํค๋ ๋ฐฉ๋ฒ์ ๋ค๋ฃน๋๋ค.
๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ์์ ํญํฌ์ ๋ฌธ์ ์ดํดํ๊ธฐ
์ด๋ฆ, ์๋ฐํ, ์ต๊ทผ ํ๋์ด ํฌํจ๋ ์ฌ์ฉ์ ํ๋กํ์ ํ์ํด์ผ ํ๋ ์๋๋ฆฌ์ค๋ฅผ ์์ํด ๋ณด์ธ์. ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์์ฐจ์ ์ผ๋ก ๊ฐ์ ธ์ค๋ฉด ์ฌ์ฉ์๋ ์ด๋ฆ์ ๋ํ ๋ก๋ฉ ์คํผ๋๋ฅผ ๋ณธ ๋ค์, ์๋ฐํ์ ๋ํ ์คํผ๋๋ฅผ ๋ณด๊ณ , ๋ง์ง๋ง์ผ๋ก ํ๋ ํผ๋์ ๋ํ ์คํผ๋๋ฅผ ๋ณด๊ฒ ๋ฉ๋๋ค. ์ด ์์ฐจ์ ์ธ ๋ก๋ฉ ํจํด์ ํญํฌ์ ํจ๊ณผ๋ฅผ ์ผ์ผ์ผ ์ ์ฒด ํ๋กํ ๋ ๋๋ง์ ์ง์ฐ์ํค๊ณ ์ฌ์ฉ์์๊ฒ ๋ถ์พ๊ฐ์ ์ค๋๋ค. ๋ค์ํ ๋คํธ์ํฌ ์๋๋ฅผ ๊ฐ์ง ํด์ธ ์ฌ์ฉ์์๊ฒ๋ ์ด ์ง์ฐ์ด ํจ์ฌ ๋ ๋๋๋ฌ์ง ์ ์์ต๋๋ค.
๋ค์์ ๊ฐ์ํ๋ ์ฝ๋ ์ค๋ํซ์ ๋๋ค:
function UserProfile() {
const name = useName(); // ์ฌ์ฉ์ ์ด๋ฆ์ ๊ฐ์ ธ์ต๋๋ค.
const avatar = useAvatar(name); // ์ด๋ฆ์ ๋ฐ๋ผ ์๋ฐํ๋ฅผ ๊ฐ์ ธ์ต๋๋ค.
const activity = useActivity(name); // ์ด๋ฆ์ ๋ฐ๋ผ ํ๋์ ๊ฐ์ ธ์ต๋๋ค.
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
์ด ์์์ useAvatar์ useActivity๋ useName์ ๊ฒฐ๊ณผ์ ์์กดํฉ๋๋ค. ์ด๋ ๋ช
ํํ ํญํฌ์ ํจ๊ณผ๋ฅผ ๋ง๋ญ๋๋ค. ์ฆ, useName์ด ์๋ฃ๋ ๋๊น์ง useAvatar์ useActivity๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ์์ํ ์ ์์ต๋๋ค. ์ด๋ ๋นํจ์จ์ ์ด๋ฉฐ ํํ ์ฑ๋ฅ ๋ณ๋ชฉ ํ์์
๋๋ค.
Suspense๋ฅผ ์ด์ฉํ ๋ณ๋ ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ์ ๋ต
Suspense๋ก ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ์ต์ ํํ๋ ํต์ฌ์ ๋ชจ๋ ๋ฐ์ดํฐ ์์ฒญ์ ๋์์ ์์ํ๋ ๊ฒ์ ๋๋ค. ๋ค์์ ์ฌ๋ฌ๋ถ์ด ํ์ฉํ ์ ์๋ ๋ช ๊ฐ์ง ์ ๋ต์ ๋๋ค:
1. React.preload ๋ฐ ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ ๋ฐ์ดํฐ ์ฌ์ ๋ก๋ฉ
๊ฐ์ฅ ๊ฐ๋ ฅํ ๊ธฐ์ ์ค ํ๋๋ ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํ๋ ๊ฒ์
๋๋ค. ์ด๋ "๋ฆฌ์์ค"(๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ํ๋ผ๋ฏธ์ค๋ฅผ ์บก์ํํ๋ ๊ฐ์ฒด)๋ฅผ ์์ฑํ๊ณ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ์ ธ์ค๋ ๊ฒ์ ํฌํจํฉ๋๋ค. React.preload๋ ์ด ์์
์ ๋์ต๋๋ค. ์ปดํฌ๋ํธ๊ฐ ๋ฐ์ดํฐ๋ฅผ ํ์๋ก ํ ๋์ฏค์๋ ์ด๋ฏธ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํ ์ ์์ด ๋ก๋ฉ ์ํ๋ฅผ ๊ฑฐ์ ์์ ํ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
์ ํ์ ๊ฐ์ ธ์ค๊ธฐ ์ํ ๋ฆฌ์์ค๋ฅผ ์๊ฐํด ๋ณด์ธ์:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// ์ฌ์ฉ๋ฒ:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
์ด์ ProductDetails ์ปดํฌ๋ํธ๊ฐ ๋ ๋๋ง๋๊ธฐ ์ ์ ์ด ๋ฆฌ์์ค๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํ ์ ์์ต๋๋ค. ์๋ฅผ ๋ค์ด, ๋ผ์ฐํธ ์ ํ ์ค์ด๋ ๋ง์ฐ์ค ์ค๋ฒ ์์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
React.preload(productResource);
์ด๋ ๊ฒ ํ๋ฉด ProductDetails ์ปดํฌ๋ํธ๊ฐ ๋ฐ์ดํฐ๋ฅผ ํ์๋ก ํ ๋ ๋ฐ์ดํฐ๊ฐ ์ด๋ฏธ ์ค๋น๋์ด ์์ด ๋ก๋ฉ ์ํ๋ฅผ ์ต์ํํ๊ฑฐ๋ ์ ๊ฑฐํ ์ ์์ต๋๋ค.
2. Promise.all์ ์ฌ์ฉํ ๋์ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
๋ ๋ค๋ฅธ ๊ฐ๋จํ๊ณ ํจ๊ณผ์ ์ธ ์ ๊ทผ ๋ฐฉ์์ Promise.all์ ์ฌ์ฉํ์ฌ ๋จ์ผ Suspense ๊ฒฝ๊ณ ๋ด์์ ๋ชจ๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ๋์์ ์์ํ๋ ๊ฒ์
๋๋ค. ์ด๋ ๋ฐ์ดํฐ ์ข
์์ฑ์ด ๋ฏธ๋ฆฌ ์๋ ค์ง ๊ฒฝ์ฐ์ ์ ์๋ํฉ๋๋ค.
์ฌ์ฉ์ ํ๋กํ ์์๋ก ๋์๊ฐ ๋ด ์๋ค. ๋ฐ์ดํฐ๋ฅผ ์์ฐจ์ ์ผ๋ก ๊ฐ์ ธ์ค๋ ๋์ , ์ด๋ฆ, ์๋ฐํ, ํ๋ ํผ๋๋ฅผ ๋์์ ๊ฐ์ ธ์ฌ ์ ์์ต๋๋ค:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: '์ฌ์ง์ ๊ฒ์ํ์ต๋๋ค.' },
{ id: 2, text: 'ํ๋กํ์ ์
๋ฐ์ดํธํ์ต๋๋ค.' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="์ฌ์ฉ์ ์๋ฐํ" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>์๋ฐํ ๋ก๋ฉ ์ค...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>ํ๋ ๋ก๋ฉ ์ค...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
๊ทธ๋ฌ๋ Avatar์ Activity ๊ฐ๊ฐ์ด fetchName์ ์์กดํ์ง๋ง ๋ณ๋์ Suspense ๊ฒฝ๊ณ ๋ด์์ ๋ ๋๋ง๋๋ ๊ฒฝ์ฐ, fetchName ํ๋ผ๋ฏธ์ค๋ฅผ ๋ถ๋ชจ๋ก ์ฌ๋ฆฌ๊ณ React Context๋ฅผ ํตํด ์ ๊ณตํ ์ ์์ต๋๋ค.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: '์ฌ์ง์ ๊ฒ์ํ์ต๋๋ค.' },
{ id: 2, text: 'ํ๋กํ์ ์
๋ฐ์ดํธํ์ต๋๋ค.' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="์ฌ์ฉ์ ์๋ฐํ" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>์๋ฐํ ๋ก๋ฉ ์ค...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>ํ๋ ๋ก๋ฉ ์ค...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. ๋ณ๋ ฌ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ๊ด๋ฆฌํ๋ ์ปค์คํ ํ ์ฌ์ฉํ๊ธฐ
์ ์ฌ์ ์ผ๋ก ์กฐ๊ฑด๋ถ ๋ฐ์ดํฐ ์ข ์์ฑ์ด ์๋ ๋ ๋ณต์กํ ์๋๋ฆฌ์ค์ ๊ฒฝ์ฐ, ๋ณ๋ ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ๊ด๋ฆฌํ๊ณ Suspense๊ฐ ์ฌ์ฉํ ์ ์๋ ๋ฆฌ์์ค๋ฅผ ๋ฐํํ๋ ์ปค์คํ ํ ์ ์์ฑํ ์ ์์ต๋๋ค.
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (!mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('๋ฆฌ์์ค๋ฅผ ์์ง ์ด๊ธฐํํ์ง ์์์ต๋๋ค.');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// ์ฌ์ฉ ์์:
async function fetchUserData(userId) {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: '์ฌ์ฉ์ ' + userId };
}
async function fetchUserPosts(userId) {
// API ํธ์ถ ์๋ฎฌ๋ ์ด์
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: '๊ฒ์๋ฌผ 1' }, { id: 2, title: '๊ฒ์๋ฌผ 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>์ฌ์ฉ์ ๋ฐ์ดํฐ ๋ก๋ฉ ์ค...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
์ด ์ ๊ทผ ๋ฐฉ์์ ํ ๋ด์์ ํ๋ผ๋ฏธ์ค ๋ฐ ๋ก๋ฉ ์ํ ๊ด๋ฆฌ์ ๋ณต์ก์ฑ์ ์บก์ํํ์ฌ ์ปดํฌ๋ํธ ์ฝ๋๋ฅผ ๋ ๊น๋ํ๊ฒ ๋ง๋ค๊ณ ๋ฐ์ดํฐ ๋ ๋๋ง์ ์ง์คํ๋๋ก ํฉ๋๋ค.
4. ์คํธ๋ฆฌ๋ฐ ์๋ฒ ๋ ๋๋ง์ ํตํ ์ ํ์ ํ์ด๋๋ ์ด์
์๋ฒ ๋ ๋๋ง ์ ํ๋ฆฌ์ผ์ด์
์ ๊ฒฝ์ฐ, React 18์ ์คํธ๋ฆฌ๋ฐ ์๋ฒ ๋ ๋๋ง์ ํตํ ์ ํ์ ํ์ด๋๋ ์ด์
์ ๋์
ํ์ต๋๋ค. ์ด๋ ์๋ฒ์์ HTML์ ์ฌ์ฉํ ์ ์๊ฒ ๋๋ ๋๋ก ํด๋ผ์ด์ธํธ์๊ฒ ์ฒญํฌ ๋จ์๋ก ๋ณด๋ผ ์ ์๋๋ก ํฉ๋๋ค. ๋๋ฆฌ๊ฒ ๋ก๋๋๋ ์ปดํฌ๋ํธ๋ฅผ <Suspense> ๊ฒฝ๊ณ๋ก ๊ฐ์ธ๋ฉด, ๋๋ฆฐ ์ปดํฌ๋ํธ๊ฐ ์๋ฒ์์ ๋ก๋๋๋ ๋์ ๋๋จธ์ง ํ์ด์ง๋ ์ํธ์์ฉ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค ์ ์์ต๋๋ค. ์ด๋ ํนํ ๋คํธ์ํฌ ์ฐ๊ฒฐ์ด ๋๋ฆฌ๊ฑฐ๋ ์ฅ์น ์ฑ๋ฅ์ด ๋ฎ์ ์ฌ์ฉ์์๊ฒ ์ฒด๊ฐ ์ฑ๋ฅ์ ๊ทน์ ์ผ๋ก ํฅ์์ํต๋๋ค.
๋ด์ค ์น์ฌ์ดํธ๊ฐ ์ธ๊ณ ์ฌ๋ฌ ์ง์ญ(์: ์์์, ์ ๋ฝ, ์๋ฉ๋ฆฌ์นด)์ ๊ธฐ์ฌ๋ฅผ ํ์ํด์ผ ํ๋ ์๋๋ฆฌ์ค๋ฅผ ์๊ฐํด ๋ณด์ธ์. ์ผ๋ถ ๋ฐ์ดํฐ ์์ค๋ ๋ค๋ฅธ ์์ค๋ณด๋ค ๋๋ฆด ์ ์์ต๋๋ค. ์ ํ์ ํ์ด๋๋ ์ด์ ์ ๋ ๋น ๋ฅธ ์ง์ญ์ ๊ธฐ์ฌ๋ฅผ ๋จผ์ ํ์ํ๊ณ , ๋๋ฆฐ ์ง์ญ์ ๊ธฐ์ฌ๊ฐ ๋ก๋๋๋ ๋์ ํ์ด์ง ์ ์ฒด๊ฐ ์ฐจ๋จ๋๋ ๊ฒ์ ๋ฐฉ์งํฉ๋๋ค.
์ค๋ฅ ๋ฐ ๋ก๋ฉ ์ํ ์ฒ๋ฆฌํ๊ธฐ
Suspense๊ฐ ๋ก๋ฉ ์ํ ๊ด๋ฆฌ๋ฅผ ๋จ์ํํ์ง๋ง, ์ค๋ฅ ์ฒ๋ฆฌ๋ ์ฌ์ ํ ์ค์ํฉ๋๋ค. ์ค๋ฅ ๊ฒฝ๊ณ(componentDidCatch ์๋ช
์ฃผ๊ธฐ ๋ฉ์๋ ๋๋ \`react-error-boundary\`์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ useErrorBoundary ํ
์ฌ์ฉ)๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ๋๋ ๋ ๋๋ง ์ค์ ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ ์ ์๋๋ก ํฉ๋๋ค. ์ด๋ฌํ ์ค๋ฅ ๊ฒฝ๊ณ๋ ํน์ Suspense ๊ฒฝ๊ณ ๋ด์์ ์ค๋ฅ๋ฅผ ์ก์๋ด์ด ์ ์ฒด ์ ํ๋ฆฌ์ผ์ด์
์ด ์ถฉ๋ํ๋ ๊ฒ์ ๋ฐฉ์งํ๋๋ก ์ ๋ต์ ์ผ๋ก ๋ฐฐ์น๋์ด์ผ ํฉ๋๋ค.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... ์ค๋ฅ๊ฐ ๋ฐ์ํ ์ ์๋ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ
}
function App() {
return (
<ErrorBoundary fallback={<div>๋ฌธ์ ๊ฐ ๋ฐ์ํ์ต๋๋ค!</div>}>
<Suspense fallback={<div>๋ก๋ฉ ์ค...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
๋ก๋ฉ ๋ฐ ์ค๋ฅ ์ํ ๋ชจ๋์ ๋ํด ์ ์ตํ๊ณ ์ฌ์ฉ์ ์นํ์ ์ธ ๋์ฒด UI๋ฅผ ์ ๊ณตํด์ผ ํฉ๋๋ค. ์ด๋ ๋คํธ์ํฌ ์๋๊ฐ ๋๋ฆฌ๊ฑฐ๋ ์ง์ญ ์๋น์ค ์ค๋จ์ ๊ฒช์ ์ ์๋ ํด์ธ ์ฌ์ฉ์์๊ฒ ํนํ ์ค์ํฉ๋๋ค.
Suspense๋ก ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ์ต์ ํํ๊ธฐ ์ํ ๋ชจ๋ฒ ์ฌ๋ก
- ์ค์ ๋ฐ์ดํฐ ์๋ณ ๋ฐ ์ฐ์ ์์ ์ง์ : ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด๊ธฐ ๋ ๋๋ง์ ํ์์ ์ธ ๋ฐ์ดํฐ๋ฅผ ํ์ ํ๊ณ ํด๋น ๋ฐ์ดํฐ๋ฅผ ๋จผ์ ๊ฐ์ ธ์ค๋๋ก ์ฐ์ ์์๋ฅผ ์ง์ ํฉ๋๋ค.
- ๊ฐ๋ฅํ ๊ฒฝ์ฐ ๋ฐ์ดํฐ ์ฌ์ ๋ก๋:
React.preload๋ฐ ๋ฆฌ์์ค๋ฅผ ์ฌ์ฉํ์ฌ ์ปดํฌ๋ํธ๊ฐ ํ์๋ก ํ๊ธฐ ์ ์ ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ ๋ก๋ํ์ฌ ๋ก๋ฉ ์ํ๋ฅผ ์ต์ํํฉ๋๋ค. - ๋ฐ์ดํฐ ๋์ ๊ฐ์ ธ์ค๊ธฐ:
Promise.all๋๋ ์ปค์คํ ํ ์ ํ์ฉํ์ฌ ์ฌ๋ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ๋ฅผ ๋ณ๋ ฌ๋ก ์์ํฉ๋๋ค. - API ์๋ํฌ์ธํธ ์ต์ ํ: API ์๋ํฌ์ธํธ๊ฐ ์ฑ๋ฅ์ ์ต์ ํ๋์ด ์๋์ง ํ์ธํ์ฌ ์ง์ฐ ์๊ฐ๊ณผ ํ์ด๋ก๋ ํฌ๊ธฐ๋ฅผ ์ต์ํํฉ๋๋ค. GraphQL๊ณผ ๊ฐ์ ๊ธฐ์ ์ ์ฌ์ฉํ์ฌ ํ์ํ ๋ฐ์ดํฐ๋ง ๊ฐ์ ธ์ค๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.
- ์บ์ฑ ๊ตฌํ: ์์ฃผ ์ก์ธ์คํ๋ ๋ฐ์ดํฐ๋ฅผ ์บ์ํ์ฌ API ์์ฒญ ์๋ฅผ ์ค์ ๋๋ค. ๊ฐ๋ ฅํ ์บ์ฑ ๊ธฐ๋ฅ์ ์ํด \`swr\` ๋๋ \`react-query\`์ ๊ฐ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ฌ์ฉ์ ๊ณ ๋ คํด ๋ณด์ธ์.
- ์ฝ๋ ์คํ๋ฆฌํ ์ฌ์ฉ: ์ด๊ธฐ ๋ก๋ ์๊ฐ์ ์ค์ด๊ธฐ ์ํด ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ ์์ ์ฒญํฌ๋ก ๋ถํ ํฉ๋๋ค. ์ฝ๋ ์คํ๋ฆฌํ ์ Suspense์ ๊ฒฐํฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ค๋ฅธ ๋ถ๋ถ์ ์ ์ง์ ์ผ๋ก ๋ก๋ํ๊ณ ๋ ๋๋งํฉ๋๋ค.
- ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง: Lighthouse ๋๋ WebPageTest์ ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ์ฌ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ฑ๋ฅ์ ์ ๊ธฐ์ ์ผ๋ก ๋ชจ๋ํฐ๋งํ์ฌ ์ฑ๋ฅ ๋ณ๋ชฉ ํ์์ ์๋ณํ๊ณ ํด๊ฒฐํฉ๋๋ค.
- ์ค๋ฅ๋ฅผ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌ: ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ๋ฐ ๋ ๋๋ง ์ค ๋ฐ์ํ๋ ์ค๋ฅ๋ฅผ ์ก์๋ด๊ณ ์ฌ์ฉ์์๊ฒ ์ ์ตํ ์ค๋ฅ ๋ฉ์์ง๋ฅผ ์ ๊ณตํ๊ธฐ ์ํด ์ค๋ฅ ๊ฒฝ๊ณ๋ฅผ ๊ตฌํํฉ๋๋ค.
- ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR) ๊ณ ๋ ค: SEO ๋ฐ ์ฑ๋ฅ์์ ์ด์ ๋ก ์คํธ๋ฆฌ๋ฐ ๋ฐ ์ ํ์ ํ์ด๋๋ ์ด์ ์ ํตํด SSR์ ์ฌ์ฉํ์ฌ ๋ ๋น ๋ฅธ ์ด๊ธฐ ๊ฒฝํ์ ์ ๊ณตํ๋ ๊ฒ์ ๊ณ ๋ คํด ๋ณด์ธ์.
๊ฒฐ๋ก
React Suspense๋ ๋ณ๋ ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ์ ๋ต๊ณผ ๊ฒฐํฉ๋ ๋, ๋ฐ์์ฑ ์๊ณ ์ฑ๋ฅ์ด ๋ฐ์ด๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๊ตฌ์ถํ๊ธฐ ์ํ ๊ฐ๋ ฅํ ๋๊ตฌ ๋ชจ์์ ์ ๊ณตํฉ๋๋ค. ํญํฌ์ ๋ฌธ์ ๋ฅผ ์ดํดํ๊ณ ์ฌ์ ๋ก๋ฉ, Promise.all์ ์ฌ์ฉํ ๋์ ๊ฐ์ ธ์ค๊ธฐ, ์ปค์คํ
ํ
๊ณผ ๊ฐ์ ๊ธฐ์ ์ ๊ตฌํํจ์ผ๋ก์จ ์ฌ์ฉ์ ๊ฒฝํ์ ํฌ๊ฒ ํฅ์์ํฌ ์ ์์ต๋๋ค. ์ ์ธ๊ณ ์ฌ์ฉ์์๊ฒ ์ ํ๋ฆฌ์ผ์ด์
์ด ์ต์ ํ๋ ์ํ๋ฅผ ์ ์งํ๋๋ก ์ค๋ฅ๋ฅผ ์ฐ์ํ๊ฒ ์ฒ๋ฆฌํ๊ณ ์ฑ๋ฅ์ ๋ชจ๋ํฐ๋งํ๋ ๊ฒ์ ์์ง ๋ง์ธ์. React๊ฐ ๊ณ์ ๋ฐ์ ํจ์ ๋ฐ๋ผ, ์คํธ๋ฆฌ๋ฐ ์๋ฒ ๋ ๋๋ง์ ํตํ ์ ํ์ ํ์ด๋๋ ์ด์
๊ณผ ๊ฐ์ ์๋ก์ด ๊ธฐ๋ฅ์ ํ์ํ๋ ๊ฒ์ ์์น๋ ๋คํธ์ํฌ ์กฐ๊ฑด์ ๊ด๊ณ์์ด ํ์ํ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ๋ ๋ฅ๋ ฅ์ ๋์ฑ ํฅ์์ํฌ ๊ฒ์
๋๋ค. ์ด๋ฌํ ๊ธฐ์ ์ ์์ฉํจ์ผ๋ก์จ ๊ธฐ๋ฅ์ ์ผ ๋ฟ๋ง ์๋๋ผ ์ ์ธ๊ณ ์ฌ์ฉ์๋ค์ด ์ฆ๊ฒ๊ฒ ์ฌ์ฉํ ์ ์๋ ์ ํ๋ฆฌ์ผ์ด์
์ ๋ง๋ค ์ ์์ต๋๋ค.
์ด ๋ธ๋ก๊ทธ ๊ฒ์๋ฌผ์ React Suspense๋ฅผ ์ฌ์ฉํ ๋ณ๋ ฌ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ ์ ๋ต์ ๋ํ ํฌ๊ด์ ์ธ ๊ฐ์๋ฅผ ์ ๊ณตํ๋ ๊ฒ์ ๋ชฉํ๋ก ํ์ต๋๋ค. ์ด ์ ๋ณด๊ฐ ์ ์ตํ๊ณ ๋์์ด ๋์ จ๊ธฐ๋ฅผ ๋ฐ๋๋๋ค. ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์์ ์ด๋ฌํ ๊ธฐ์ ์ ์คํํ๊ณ ๊ฒฐ๊ณผ๋ฅผ ์ปค๋ฎค๋ํฐ์ ๊ณต์ ํ์๊ธฐ๋ฅผ ๊ถ์ฅํฉ๋๋ค.